libvirt 虚拟机内存管理
十月 29, 2025 更新: 十月 11, 2026 [虚拟化与容器] #libvirt #ballooninglibvirt windows虚拟机内存占用过高
虚拟机内存策略
- QEMU 在虚拟机启动时,一次性分配固定的内存。 这部分内存被 预留并映射到宿主机进程(qemu-system-x86_64)。 从宿主机的角度看,这些内存都是“被占用的”。 即使 Windows 内部只使用很少,也不会被宿主机回收。
- 💡 换句话说: 没有 balloon 驱动 = 没有动态回收机制。 QEMU 不知道 Windows 里面的内存使用情况,它只能把所有内存都“留给”虚拟机进程。
- Windows 内存管理的特性加剧了现象 Windows 客户机即便空闲,也会: 启动缓存与系统文件预取; 启动 SuperFetch / SysMain; 各种服务预分配内存池; 内核会尽量“占用”空闲内存以提高 I/O 性能(File Cache)。 因此在任务管理器中看似“使用很多内存”,其实很多是 可释放缓存(Standby Memory)。 但宿主机(Linux + libvirt)看不到“哪些是可回收的”,于是整块都算“已用内存”。
Ballooning 如何解决这个问题?
Ballooning 的作用是 建立 Guest 与 Host 之间的内存使用反馈通道:
-
Guest 内 balloon 驱动 作为一个“伪设备”,可以向 Windows 内存管理器 申请分配内存(就像一个普通程序申请内存一样);
-
Windows 会从其空闲内存池中分配给 balloon 驱动;
-
但 balloon 驱动 不会真正使用这些内存,只是“持有”它;
-
由于这部分内存被 balloon 占用,QEMU 就知道:Guest 实际可用内存减少了,可以将对应物理页从宿主机释放;
-
宿主机内存压力大时,libvirt 可通过 virsh setmem 命令 增大 balloon 大小,从而“回收”虚拟机内存。
-
Linux 虚拟机也一样:没有 balloon 驱动(如 virtio_balloon 模块未加载),同样会高占内存;
-
现代替代方案:virtio-mem(动态内存插拔)比 balloon 更灵活,但 Windows 支持有限,balloon 仍是主流;
-
内存共享(KSM)不能替代 balloon:KSM 只能合并完全相同的内存页(如多个 Win10 虚拟机的系统 DLL),对私有数据无效。
libvirt/QEMU 支持 Windows 虚拟机的内存 ballooning(动态内存管理)
需要在 宿主机与客户机两边都正确配置
- windows 虚拟机上安装 virtio-win-xxx.iso 中的ballooning(其他的可以不安装,容易引起问题)
- 配置虚拟机配置
<memballoon model='virtio'>
<stats period='10'/>
</memballoon>
实战:Linux guest 内存 32G 恒 100%,cockpit 与 htop 打架(2026-10 更新)
现象
一台 32G 的 Ubuntu 24.04 开发机(16 vCPU,跑了一年):
- guest 内 htop:6.13G / 31.3G(约 20%);
- cockpit-machines 占用率卡片:32.0 / 32 GiB,恒 100%;
- 宿主机
ps:QEMU 进程 RSS ≈ 32 GiB。
第一反应是"是不是没装 qemu-guest-agent / 没有 memballoon"——都装了且工作正常:
$ virsh dommemstat ubuntu.code
actual 33554432
unused 32681168 ← balloon 驱动提供,guest 空闲 31.2G
available 32781204
rss 33512528 ← QEMU 进程 RSS,涨满
swap_in/out、major/minor_fault、disk_caches ... ← qemu-guest-agent 提供
$ virsh qemu-agent-command ubuntu.code '{"execute":"guest-ping"}'
{"return":{}}
cockpit 那张卡片的真实语义
解包前端(gunzip -c /usr/share/cockpit/machines/index.js.gz)找到采集逻辑:cockpit-machines
调用 org.libvirt.Domain 的 GetStats(flags=CPU_TOTAL|BALLOON|VCPU, 0),从返回里取
balloon.rss(QEMU 进程在宿主机上的 RSS),除以 XML 的 currentMemory:
内存占用 % = balloon.rss / currentMemory ← 宿主机视角,不是 guest 视角
(busctl 实测掩码:1=STATE、2=CPU_TOTAL、4=BALLOON、8=VCPU、16=BLOCK;
virsh dommemstat 的 unused/available 才是 guest 视角。)
为什么 RSS 只涨不跌
三个事实叠加:
- guest 每摸过一页内存,这页就永久留在 QEMU 的 RSS 里——包括 page cache;
- guest 内核"闲着不如缓存",把 25G 拿去做 page cache,完全合理;
- balloon 从不自动归还:没有主动挤气球动作,QEMU RSS 只增不减。
于是 guest 用 6G、cache 25G、RSS 32G、cockpit 恒 100%——每一层都"没坏",是机制如此。
解法:freePageReporting(自动归还)
<memballoon model='virtio' freePageReporting='on'>
<stats period='5'/>
</memballoon>
guest 内核周期性把空闲页报告给 QEMU(madvise DONTNEED),宿主机 RSS 自动回落;
guest 需要时立刻拿回(弹性归还,不像 setmem 是硬上限)。要求:guest 内核 ≥5.7、
libvirt ≥6.10、QEMU 设备属性存在(qemu-system-x86_64 -device virtio-balloon-pci,help
可见 free-page-reporting)。生效需要关机再开机(memballoon 不能热改属性)。
对比:
virsh setmem | freePageReporting | |
|---|---|---|
| 语义 | 硬上限(guest 可见内存变小) | 弹性归还(guest 内存不缩水) |
| 生效 | 立即,可逆 | 需重启一次,之后全自动 |
| cockpit 卡片 | 仍 ~100%(分母同步变小) | 跟随真实占用动态变化 |
三个坑(define 静默剥离实录)
freePageReporting是<memballoon>的属性,不是子元素。写成子元素<freePageReporting on='on'/>时virsh define不报错但静默剥离——不带--validate的 define 不跑 RNG schema,宽松解析器直接忽略不认识的元素。加了--validate才会报Extra element devices in interleave。- 漏写
on='on'也不行:<freePageReporting/>解析为默认 off,格式化输出时省略, 同样静默消失。 - define 会顺带丢
<stats period>(balloon 统计采集周期,cockpit/dommemstat 依赖), define 后必须virsh dumpxml --inactive <vm>复查。
define 成功的判据不是命令退出码,而是复查存储结果:
virsh define --validate /path/to/vm.xml
virsh dumpxml --inactive <vm> | grep -A3 '<memballoon' # 必须肉眼确认
重启后验证:
pgrep -f 'qemu-system.*<vm>' | head -1 | xargs -I{} cat /proc/{}/cmdline | tr '\0' '\n' | grep free-page-reporting
# → free-page-reporting=on
相关阅读
- 同一台 VM 的 cockpit 图形 console 消失问题: cockpit-machines 只剩串口 console